package tasky.storage;

import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.JsonDeserializationContext;
import com.google.gson.JsonDeserializer;
import com.google.gson.JsonElement;
import com.google.gson.JsonParseException;
import com.google.gson.JsonPrimitive;
import com.google.gson.JsonSerializationContext;
import com.google.gson.JsonSerializer;
import com.google.gson.reflect.TypeToken;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.FileReader;
import java.io.BufferedReader;
import java.io.FileWriter;
import java.io.BufferedWriter;
import java.io.IOException;
import java.io.PrintWriter;
import java.lang.reflect.Type;
import java.util.ArrayList;
import java.util.Scanner;
import java.util.logging.FileHandler;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.logging.SimpleFormatter;

import org.joda.time.DateTime;

import tasky.commons.Task;



public class Storage {

	static Gson gson;
	ArrayList<Task> tasks;
	File taskFile;
	File recoveryFile;
	File helpFile;
	static final String HELP_FILE_NAME = "help.txt";
	static final String TASK_FILE_NAME = "taskfile.txt";
	static final String RECOVERY_FILE_NAME = "recoveryfile.txt";
	
	private static Storage storage = null;

	private static Logger logger = null;

	// static class to help convert DateTime object from joda library to JSON format
	static class DateTimeTypeConverter implements JsonSerializer<DateTime>,
			JsonDeserializer<DateTime> {
		// No need for an InstanceCreator since DateTime provides a no-args
		// constructor

		@Override
		public JsonElement serialize(DateTime src, Type srcType, JsonSerializationContext context) {
			
			return new JsonPrimitive(src.toString());
		}

		@Override
		public DateTime deserialize(JsonElement json, Type type, JsonDeserializationContext context) throws JsonParseException {
			
			return new DateTime(json.getAsString());
		}
	}
	

	// Private Constructor for Storage class
	private Storage() throws IOException, ClassNotFoundException {
		
		initialiseFiles();
		initialiseLog();
		gson = initialiseGson();
		writeToTasks();
		
		logger.info("Storage instance created");

	}
	
	// getInstance method for Storage singleton
	public static Storage getInstance() throws IOException, ClassNotFoundException {
		if(storage == null) {
			storage = new Storage();
		}
		
		return storage;
	}
	
	// helper method for constructor to initialize taskFile and recoveryFile
	private void initialiseFiles() throws IOException {
		taskFile = openFile(TASK_FILE_NAME);
		recoveryFile = openFile(RECOVERY_FILE_NAME);
		helpFile = openFile(HELP_FILE_NAME);
		
		//logger.info("The taskFile and the recoveryFile opened");
	}
	
	// help method for initialiseFiles to open file
	private static File openFile(String fileName) throws IOException {
		
		File file = null;

		try {
			file = new File(fileName);

			if (!file.exists()) {
				file.createNewFile();
			}
		} catch (IOException e) {
			logger.log(Level.SEVERE, "Error occurred in opening file", e);
			e.printStackTrace();
			System.exit(1);
		}
		
		return file;
	}
	
	// helper method for constructor to initialize logging
	private void initialiseLog() throws IOException {
		logger = Logger.getLogger(Storage.class.getName());
		FileHandler storageFileHandler = new FileHandler("Storage_logFile.log");
		logger.addHandler(storageFileHandler);
		storageFileHandler.setFormatter(new SimpleFormatter());

	}
	
	// helper method for constructor to initialize gson
	private Gson initialiseGson() {
		GsonBuilder builder = new GsonBuilder();
		builder.registerTypeAdapter(DateTime.class, new DateTimeTypeConverter());
		logger.info("Gson initilized");
		
		return builder.setPrettyPrinting().create();
		
	}

	// write taskList to task file
	public String writeToStorage(ArrayList<Task> taskList) throws IOException {
		BufferedWriter writer = null;
		
		try {
			String json = gson.toJson(taskList);
			writer = new BufferedWriter(new FileWriter(TASK_FILE_NAME));
			writer.write(json);
			logger.info("TaskList has been written to Storage");
			
			return "saved";
			
		} catch (IOException e) {
			logger.log(Level.SEVERE, "Error occured in writing to task file", e);
			e.printStackTrace();
			
			return "not saved";	
			
		} finally {
			if (writer != null) {
				writer.close();
			}
		}	
	}

	// make a copy of content of task file to recovery file
	public void writeToRecovery(ArrayList<Task> taskList) throws IOException {
		BufferedWriter writer = null;
		
		try {
			//mark recovery file as writable
			recoveryFile.setWritable(true);
			
			String json = gson.toJson(taskList);
			writer = new BufferedWriter(new FileWriter(recoveryFile));
			writer.write(json);
			
			// mark recoveryFile as read only
			recoveryFile.setReadOnly();
			
			logger.info("Write to recovery file is successful");
			
		} catch (IOException e) {
			logger.log(Level.SEVERE, "Error occurred in writing to recovery file", e);
			e.printStackTrace();
			
		} finally {
			if(writer != null) {
				writer.close();
			}
		}
	}
	
	// write tasks stored in task file to the arrayList<Task> tasks
	protected boolean writeToTasks() throws IOException, ClassNotFoundException {
		BufferedReader reader = null;
		
		try {
			reader = new BufferedReader(new FileReader(taskFile));

			tasks = gson.fromJson(reader, new TypeToken<ArrayList<Task>>() {
			}.getType());

			if (tasks == null) {
				tasks = new ArrayList<Task>();
			}
			logger.info("Write to tasks is succesful");
			// if no exception is throw, return true
			return true;

		} catch (IOException e) {
			logger.log(Level.SEVERE, "Error occurred in writing to tasks",e);
			e.printStackTrace();
			
			return false;
			
		} catch (RuntimeException ex) {
			logger.log(Level.SEVERE, "Storage file is corrupted. File content is restored to previous session",ex);
			ex.printStackTrace();
			
			// call refreshFile method when task file is no longer in JSON format 
			refreshFile();
			
			return false;
		} finally {
			if (reader != null) {
				reader.close();
			}
		}
		
	}

	public ArrayList<Task> getTasks() {
		return tasks;
	}

	// This method will be called when user make invalid changes to the task
	// file causing it to be corrupted.
	// The task file will be emptied first.
	// The content of recovery file will then be written to task file.
	// The result is to cause the state of the task file to return to last session.
	private void refreshFile() throws IOException, ClassNotFoundException {
		PrintWriter pw = null;
		
		try {
			// empty taskFile
			pw = new PrintWriter(taskFile);
			
			recoveryFile.setWritable(true);
			// restore tasks from recoveryFile
			taskFile = new File(RECOVERY_FILE_NAME);
			writeToTasks();
			recoveryFile.setReadOnly();
			logger.info("Refresh file successful");

		} catch (Exception e) {
			logger.log(Level.SEVERE, "Error occurred in refreshing file", e);
			e.printStackTrace();
			
		} finally {
			pw.close();
		}
	}

	// helper method for exportTaskFile and importTaskFile
	private static String determineFileName(String name) {
		String filename = null;
		if(name == null || name.equals("")) {
			filename = TASK_FILE_NAME;
		} else {
			filename = name;
		}
		
		return filename;
	}
	
	// export task file to desktop.
	public boolean exportTaskFile(String name) throws IOException {
		String desktopPath = DesktopPath.getCurrentUserDesktopPath();
		String filename = determineFileName(name);
		FileInputStream fis = null;
		FileOutputStream fos = null;
		
		try {
			File destinationFile = new File(desktopPath + "\\" + filename);
			fis = new FileInputStream(taskFile);
			fos = new FileOutputStream(destinationFile);
			byte[] buffer = new byte[2048];
			int length;
			while ((length = fis.read(buffer)) > 0) {
				fos.write(buffer, 0, length);
			}
			
			logger.info("Export file succesful");
			
			return true;

		} catch (IOException e) {
			logger.log(Level.SEVERE, "Error occurred in export", e);
			e.printStackTrace();
			
			return false;

		} finally {
			if (fis != null) {
				fis.close();
			} 
			if (fos != null) {
				fos.close();
			}
		}
		
	}
	
	
	
	// import task file from desktop
	public boolean importTaskFile(String name) throws IOException, ClassNotFoundException {
		String desktopPath = DesktopPath.getCurrentUserDesktopPath();
		String filename = determineFileName(name);
		
			
		taskFile = new File(desktopPath + "\\" + filename);
		
		// if originFile doesn't exist before
		if(taskFile.length() == 0) {
			taskFile.delete();
			return false;
		}
		
		if(writeToTasks()) {
			logger.info("Import file successful");
			return true;
		}
		else {
			return false;
		}
	}
	
	
	// return content of help file as a String
	public String getHelp() throws IOException {
		StringBuilder fileContents = null;
		Scanner scanner = null;
		try {
			
			//mark help file as read only
			helpFile.setReadOnly();
			
			fileContents = new StringBuilder((int) helpFile.length());
			scanner = new Scanner(new BufferedReader(new FileReader(helpFile)));
			String lineSeparator = "\n";

			while (scanner.hasNextLine()) {
				fileContents.append(scanner.nextLine()).append(lineSeparator);
			}

		} catch (IOException e) {
			logger.log(Level.SEVERE, "Error occurred in opening help file", e);
			e.printStackTrace();
		} finally {
			scanner.close();
		}
		logger.info("Get help succesful");
		
		return fileContents.toString();
	}

}
